﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using NMapper.Utils;

namespace NMapper.Mappings
{
    public class DenormalizeMapping<TDatabase, TDatabaseEntity, TOutput, TChildDbEntity> : IMapping<TDatabase>
        where TDatabase : IDisposable
        where TDatabaseEntity : class
        where TChildDbEntity : class
    {
        private Expression<Func<TDatabaseEntity, TChildDbEntity>> dbSelector;
        private Func<TDatabaseEntity, TChildDbEntity> dbSelectorFunc;
        private IMapper<TDatabase, TChildDbEntity, TOutput> mapper;
        private PropertyInfo property;

        public DenormalizeMapping(Expression<Func<TDatabaseEntity, TChildDbEntity>> dbSelector, IMapper<TDatabase, TChildDbEntity, TOutput> mapper)
        {
            this.dbSelector = dbSelector;
            dbSelectorFunc = dbSelector.Compile();
            this.mapper = mapper;
        }

        public PropertyInfo TransientProperty
        {
            get { return property; }
        }

        public void DefineTransientType(ModuleBuilder module, TypeBuilder type)
        {
            mapper.Initialize();
            var propertyName = NameHelpers.SuggestName(dbSelector);
            property = type.DefineProperty(propertyName, mapper.TransientType);
        }

        public void DefineTransientInitialization(MapContext<TDatabase> mapContext, Type transientType, List<MemberBinding> initializers, ParameterExpression parameterExpression)
        {
            property = transientType.GetProperty(property.Name);

            var lambdaBinder = new LambdaBinder();
            var subSelector = mapper.CreateTransientSelector(mapContext);
            var newTarget = lambdaBinder.BindBody(dbSelector, parameterExpression);
            var boundSubSelector = lambdaBinder.BindBody(subSelector, newTarget);

            var nullChecker = new LambdaNullChecker();
            Expression boundSubSelectorWithNullCheck = nullChecker.AddNullCheck(newTarget, boundSubSelector);
            
            var initializer = Expression.Bind(property.GetSetMethod(), boundSubSelectorWithNullCheck);
            initializers.Add(initializer);
        }

        void IMapping<TDatabase>.OnTransientInitialized(MapContext<TDatabase> mapContext, Array transient)
        {
            foreach (var item in transient)
            {
                var value = property.GetValue(item, null);
                mapper.OnTransientInitialized(mapContext, value != null ? new[] { value } : new object[0]);                
            }
        }

        public void MapToOutput(MapContext<TDatabase> mapContext, object input, object output)
        {
            var value = property.GetValue(input, null);

            if (value != null)
                mapper.MapToOutput(mapContext, input, output);
        }

        public void MapToInput(MapContext<TDatabase> mapContext, object output, object input)
        {
            var existingEntity = dbSelectorFunc((TDatabaseEntity)input);

            var dbProperty = dbSelector.GetPropertyInfo();
            if (dbProperty != null)
            {
                var entity = mapper
                    .TransformToInput(mapContext, new[] { output }, ids => existingEntity != null ? new[] { existingEntity } : new TChildDbEntity[0])
                    .SingleOrDefault();                    
                if (existingEntity == null)
                    dbProperty.SetValue(input, entity, null);
            }
        }

        public override string ToString()
        {
            return string.Format("DenormalizeMapping({0}, {1})", dbSelector, mapper);
        }
    }         
}